#!/usr/bin/python
# -*- coding: utf-8 -*-

u"""
This is the game base class for pygame. It has the same state managment as the
GambeBase. It translates the pygame events into the events of the GameState.
By doing so, there are some differences between the arguments of the single 
events and there are additional convinient events defined in GameState. If you
want to use the pygame event queue you should be aware, that PygameGameBase
uses all events defined in PYGAME_EVENT_TYPES. This includes also 
EVENT_UPDDATE_ID and EVENT_SCHEDULE_ID (pygame.USEREVENT and pygame.USEREVENT+1, 
dont use them, since this would confuse the processing). This two events are 
define as pygame.USEREVENT and pygame.USEREVENT + 1, so avoid this values in 
your code.
"""

__version__ = '$Id: pygamebase.py 257 2009-04-23 21:02:16Z dr0iddr0id $'

import sys
import os

import pygame

# optimizations
from pygame.locals import QUIT
from pygame.locals import ACTIVEEVENT
from pygame.locals import KEYDOWN
from pygame.locals import KEYUP
from pygame.locals import MOUSEMOTION
from pygame.locals import MOUSEBUTTONUP
from pygame.locals import MOUSEBUTTONDOWN
from pygame.locals import JOYAXISMOTION
from pygame.locals import JOYBALLMOTION
from pygame.locals import JOYHATMOTION
from pygame.locals import JOYBUTTONUP
from pygame.locals import JOYBUTTONDOWN
from pygame.locals import VIDEORESIZE
from pygame.locals import VIDEOEXPOSE

from pygame.event import peek as event_peek
from pygame.event import get as event_get
from pygame.event import wait as event_wait
from pygame.time import set_timer
from pygame.time import get_ticks

# local imports
from bipy import gamebase
from bipy.events import Event


# define two new event id's for PygameGameBase
EVENT_UPDDATE_ID = pygame.USEREVENT
EVENT_SCHEDULE_ID = pygame.USEREVENT + 1

# define all used eventtypes of pygame
PYGAME_EVENT_TYPES = [
    pygame.QUIT,           #  none
    pygame.ACTIVEEVENT,    #  gain, state
    pygame.KEYDOWN,        #  unicode, key, mod
    pygame.KEYUP,          #  key, mod
    pygame.MOUSEMOTION,    #  pos, rel, buttons
    pygame.MOUSEBUTTONUP,  #  pos, button
    pygame.MOUSEBUTTONDOWN,#  pos, button
    pygame.JOYAXISMOTION,  #  joy, axis, value
    pygame.JOYBALLMOTION,  #  joy, ball, rel
    pygame.JOYHATMOTION,   #  joy, hat, value
    pygame.JOYBUTTONUP,    #  joy, button
    pygame.JOYBUTTONDOWN,  #  joy, button
    pygame.VIDEORESIZE,    #  size, w, h
    pygame.VIDEOEXPOSE,    #  none
    EVENT_UPDDATE_ID,
    EVENT_SCHEDULE_ID
]

class PygameGameBase(gamebase.GameBase):
    u"""
    Base class for a pygame based game.

    
    If this game should be updated 'manually', then the only method that has 
    to be called is _handle_events(event_list, current_time).
    
    If you want to use user defined events then you should not use
    pygame.USEREVENT and pygame.USEREVENT+1 directy, because this two events
    are use by this gamebase. In additionally you should add the userevent to
    the PYGAME_EVENT_TYPES list to get optimal event processing. Since the 
    pygame events are translated into this own eventsystem any new (user defined) 
    event unkown will activate the event_unkown in the the state.
    """

    def __init__(self, first_state, state_args=[], 
                 state_kwargs={}, 
                 size=(800, 600), 
                 flags=(pygame.RESIZABLE), 
                 caption = None, 
                 *args, 
                 **kwargs):
        u"""
        Same arguments as GameBase.
        Additionally: size and flags are the arugments for pygame.display.set_mode().
        Access to the surface of the screen is given by the attribute 'screen'.
        """
        #TODO: maybe screen should be probagated with on_render!?        
        super(PygameGameBase, self).__init__(first_state, state_args, state_kwargs, *args, **kwargs)
        self._schedule_cb = lambda: None
        self._time = 0
        if __debug__:
            self._num_updates = 0.0
        self.size = size
        self.flags = flags
        self.caption = caption

    #- override -#
    def _schedule_once(self, func, dt):
        u"""
        Internal use. pygame specific implementation for the scheduler interface.
        """
        set_timer(EVENT_SCHEDULE_ID, dt)
        self._schedule_cb = func

    #- override -#
    def _unschedule(self, func):
        u"""
        Internal use. pygame specific implementation for the scheduler interface.
        """
        set_timer(EVENT_SCHEDULE_ID, 0)
        self._schedule_cb = lambda: None

    #- override -#
    def _get_time(self):
        u"""
        Return time since start [ms].
        """
        return get_ticks()

    #- override -#
    def _handle_events(self, *args, **kwargs):
        u"""
        Internal use. pygame specific implementation for translating events.
        """
        cur_state = self._cur_state # optimization
        for event in args[0]:
            # TODO: use event any?
            #cur_state.event_any(event)
            event_type = event.type # optimization
            #-- events should be arranged most frequent first --#
            if event_type == EVENT_SCHEDULE_ID:
                self._schedule_cb()
            elif event_type == EVENT_UPDDATE_ID:
                dt = args[1] - self._time
                if dt > 0:
                    if __debug__:
                        self._num_updates += 1
                        sys.stdout.write(u"dt: %i  t: %i  _t:  %i  fps: %f" %(dt, args[1], self._time, 1000./dt if dt>0 else 0))
                    # TODO: filter to big dt values?
                    # self._time = get_ticks() instead of t ? no you loose time
                    self._update(dt, args[1])
                    self._time = args[1]
            
            #-- mouse --#
            #    pygame.MOUSEMOTION,    #  pos, rel, buttons
            elif event_type == MOUSEMOTION:
                if event.buttons == (0, 0, 0): # unpressed generate a motion event
                    cur_state.event_mouse_motion(event.pos, event.rel)
                else: # pressed generate a drag event
                    cur_state.event_mouse_drag(event.pos, event.rel, event.buttons, pygame.key.get_mods())
            #    pygame.MOUSEBUTTONUP,  #  pos, button
            elif event_type == MOUSEBUTTONUP:
                cur_state.event_mouse_button_up(event.pos, event.button, pygame.key.get_mods())
            #    pygame.MOUSEBUTTONDOWN,#  pos, button
            elif event_type == MOUSEBUTTONDOWN:
                cur_state.event_mouse_button_down(event.pos, event.button, pygame.key.get_mods())
            
            #-- keyboard --#
            #    pygame.KEYDOWN,        #  unicode, key, mod
            elif event_type == KEYDOWN:
                cur_state.event_key_down(event.key, event.mod, event.unicode)
            #    pygame.KEYUP,          #  key, mod
            elif event_type == KEYUP:
                cur_state.event_key_up(event.key, event.mod)
            #-- joystick --#
            #    pygame.JOYAXISMOTION,  #  joy, axis, value
            elif event_type == JOYAXISMOTION:
                cur_state.event_joy_axis_motion(event.joy, event.axis, event.value)
            #    pygame.JOYBALLMOTION,  #  joy, ball, rel
            elif event_type == JOYBALLMOTION:
                cur_state.event_joy_ball_motion(event.joy, event.ball, event.rel)
            #    pygame.JOYHATMOTION,   #  joy, hat, value
            elif event_type == JOYHATMOTION:
                cur_state.event_joy_hat_motion(event.joy, event.hat, event.value)
            #    pygame.JOYBUTTONUP,    #  joy, button
            elif event_type == JOYBUTTONUP:
                cur_state.event_joy_button_up(event.joy, event.button)
            #    pygame.JOYBUTTONDOWN,  #  joy, button
            elif event_type == JOYBUTTONDOWN:
                cur_state.event_joy_button_down(event.joy, event.button)
            
            #    pygame.QUIT,           #  none
            elif event_type == QUIT:
                cur_state.event_quit(self)
            #    pygame.ACTIVEEVENT,    #  gain, state
            elif event_type == ACTIVEEVENT:
                cur_state.event_active_event(event.state, event.gain)
                if (event.state & 0x1): # mouse bit
                    if event.gain: # mouse enter
                        cur_state.event_mouse_enter(pygame.mouse.get_pos())
                    else: # mouse leave
                        cur_state.event_mouse_leave(pygame.mouse.get_pos())
                # Note: this works unreliable!!
#                        if (event.state & 0x4): # app bit (iconify/restore)
#                            if event.gain: # restored
#                                pass 
#                            else: # iconified
#                                pass 
                if (event.state & 0x2): # key bit
                    if event.gain: # get keyboard focus
                        cur_state.event_activate()
                    else: # lost keyboard focus
                        cur_state.event_deactivate()
            
            #-- video --#
            #    pygame.VIDEORESIZE,    #  size, w, h
            elif event_type == VIDEORESIZE:
                cur_state.event_video_resize(event.w, event.h)
            #    pygame.VIDEOEXPOSE,    #  none
            elif event_type == VIDEOEXPOSE:
                cur_state.event_video_expose()
            else:
                cur_state.event_unknown(event)

    #- override -#
    def _on_framerate_changed(self, tpf):
        u"""
        Internal use. Adjusting the fps of pygame.
        """
        if self._tpf > 0:
            pygame.time.set_timer(EVENT_UPDDATE_ID, int(round(self._tpf)))
        else:
            pygame.time.set_timer(EVENT_UPDDATE_ID, 0) # only events

    #- override -#
    def run(self):
        u"""
        Start the game. pygame specific main loop.
        """
        # init pygame
        pygame.init()
        # set initial fps
        self.framerate = self.framerate # just to activate the event
        # optimization
        handle_events = self._handle_events
        start_time = get_ticks()
        # clear any event in the queue before start
        pygame.event.clear() 
        # set up screen
        self.screen = pygame.display.set_mode(self.size, self.flags)
        if self.caption:
            pygame.display.set_caption(self.caption)
        if __debug__:
            start_time = get_ticks()
            sys.stdout.write(u"%s starttime: %i" % (os.linesep, start_time, ))
        # main loop
        self._update = self._first_update
        self._update(self._tpf, start_time)
        self._time = start_time
        while self._running:
            if(event_peek(PYGAME_EVENT_TYPES)):
                events = event_get()
            else:
                self._cur_state.event_idle()
                events = [event_wait()] # blocking
            t = get_ticks()
            handle_events(events, t)
        # stop running
        pygame.time.set_timer(EVENT_UPDDATE_ID, 0)
        pygame.time.set_timer(EVENT_SCHEDULE_ID, 0)
        if __debug__:
            sys.stdout.write(u"%s running time: %i  actual ticks: %i  expected framerate: %f" % (os.linesep, self._time, get_ticks(), self.framerate))
            sys.stdout.write(u"%s num update: %i  avg fps: %f" %(os.linesep, self._num_updates, self._num_updates / (self._time - start_time) * 1000.0))
        pygame.quit()


#-------------------------------------------------------------------------------
class PygameState(gamebase.GameState):
    u"""
    Base state for pygame based games. Adds pygame specifig events.
    For the other events see gamebase.GameState.
    
    pygame specific input events::
    
        self.event_active_event     on_active_event(gain, state)
        self.event_joy_axis_motion  on_joy_axis_motion(joy, axis, value)
        self.event_joy_ball_motion  on_joy_ball_motion(joy, ball, rel)
        self.event_joy_hat_motion   on_joy_hat_motion(joy, hat, value)
        self.event_joy_button_up    on_joy_button_up(joy, button)
        self.event_joy_button_down  on_joy_button_down(joy, button)
    
    """
    def __init__(self, *args, **kwargs):
        u"""
        State init.
        """
        super(PygameState, self).__init__(*args, **kwargs)
        #-- default pygame events --#
        self.event_active_event = Event(u'event_active_event')#       pygame.ACTIVEEVENT,    #  gain, state
        self.event_joy_axis_motion = Event(u'event_joy_axis_motion')#    pygame.JOYAXISMOTION,  #  joy, axis, value
        self.event_joy_ball_motion = Event(u'event_joy_ball_motion')#    pygame.JOYBALLMOTION,  #  joy, ball, rel
        self.event_joy_hat_motion = Event(u'event_joy_hat_motion')#     pygame.JOYHATMOTION,   #  joy, hat, value
        self.event_joy_button_up = Event(u'event_joy_button_up')#      pygame.JOYBUTTONUP,    #  joy, button
        self.event_joy_button_down = Event(u'event_joy_button_down')#    pygame.JOYBUTTONDOWN,  #  joy, button
        
    
#-------------------------------------------------------------------------------

def main():
    game = PygameGameBase(PygameState())
    import bipy.gamebase
    print '@'*10, bipy.gamebase.TheGameBase == game, bipy.gamebase.TheGameBase,  game
    #-- blocking: this is needed if you only want to react on events --#
    #game.framerate = 0
#    def on_close(g):
#        g.quit()
#        g._update(1, 1)
#    game.get_game_state().event_close += on_close
    #-- blocking --#
    # GameTime scheduler does not work in blocking mode
    def update(gdt, gt, dt, t):
        print 'time update', gdt, gt, dt, t
#    game.get_game_state().game_time.event_update += update
#    def cb(i, j):
#        i+j
#        #print 'cb called %i times and %i repeats' % (i, j)
#    for i in range(100):
#        for j in range(50):
#            game.get_game_state().game_time.schedule(i*100, cb, i, j)
    game.run()

if __name__ == "__main__":
    import sys
    import traceback
    if "profile" in sys.argv:
        import hotshot
        import hotshot.stats
        import tempfile
        import os
 
        profile_data_fname = tempfile.mktemp("prf")
        try:
            prof = hotshot.Profile(profile_data_fname)
            prof.run('main()')
            del prof
            s = hotshot.stats.load(profile_data_fname)
#            s.strip_dirs()
            sys.stdout.write(u"cumulative %s%s" %(os.linesep, os.linesep))
            s.sort_stats('cumulative').print_stats()
            sys.stdout.write(u"By time. %s%s" %(os.linesep, os.linesep))
            s.sort_stats('time').print_stats()
            del s
        finally:
            # clean up the temporary file name.
            try:
                os.remove(profile_data_fname)
            except:
                # may have trouble deleting ;)
                pass
    else:
        try:
            main()
        except:
            traceback.print_exc(sys.stderr)

